package com.lili.core.bridge;

import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.lili.core.bridge.annotation.Locations;
import com.lili.core.bridge.annotation.Parent;
import com.lili.core.log4lili.Logger;
import com.lili.core.log4lili.LoggerFactory;

/**
 * ModuleRegistry provides an easy way to initialize Modules.
 */
public class ModuleRegistry {

	private ModuleRegistry() {
	}

	public static ModuleRegistry getInstance() {
		return _instance;
	}

	private static ModuleRegistry _instance = new ModuleRegistry();

	/**
	 * Returns module by type.
	 *
	 * @param <T>
	 * @param moduleType
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public <T> T getModule(Class<T> moduleType) {
		return (T) (_modules.containsKey(moduleType) ? _modules.get(moduleType)
				: registerModule(moduleType));
	}

	/**
	 * Register module. The module's parent module will also be registered. The
	 * application context's parent will be the context which holds the
	 * ModuleRegistry if the module is top one (does not have a parent).
	 *
	 * @param moduleType
	 *            class of the module to be registered.
	 * @return the module just registered.
	 */
	protected Object registerModule(Class<?> moduleType) {
		return registerModule(moduleType, null);
	}

	protected synchronized Object registerModule(Class<?> moduleType,
			ApplicationContext parentContext) {
		if (_modules.containsKey(moduleType)) {
			return getModule(moduleType);
		}

		if (moduleType.isAnnotationPresent(Parent.class)) {
			Parent parentAnnonation = moduleType.getAnnotation(Parent.class);
			Class<?> parentType = parentAnnonation.value();
			Object parentModule = registerModule(parentType);
			parentContext = ((ModuleProxy) Proxy
					.getInvocationHandler(parentModule)).getContext();
		}

		if (parentContext == null) {
			parentContext = _rootContext;
		}

		Object module = createModuleProxy(moduleType, parentContext);
		_modules.put(moduleType, module);

		if (_logger.isDebugEnabled()) {
			_logger.debug("Register module (%s)", moduleType);
		}

		return module;
	}

	//

	/**
	 * Create the module and it application context. Only
	 * ClassPathXmlApplicationContext supported.
	 */
	protected Object createModuleProxy(Class<?> moduleType,
			ApplicationContext parent) {
		String[] paths = getContextLocation(moduleType);
		ApplicationContext context = new ClassPathXmlApplicationContext(paths,
				parent);

		try {
			return ModuleProxy.newProxyInstance(moduleType, context);
		} catch (Exception e) {
			throw new ApplicationContextException("Unable to instance module: "
					+ moduleType.getName(), e);
		}
	}

	/**
	 * Returns the location of xml resource where context will load. By default
	 * the path is {@code <i>ModuleInterface</i>Context.xml}.
	 *
	 * @param moduleType
	 * @return
	 */
	protected String[] getContextLocation(Class<?> moduleType) {
		if (moduleType.isAnnotationPresent(Locations.class)) {
			Locations locationAnnonation = moduleType
					.getAnnotation(Locations.class);
			return locationAnnonation.value();
		}
		String path = String.format("classpath:%s.xml", moduleType.getName()
				.replace('.', '/'));
		return new String[] { path };
	}

	/**
	 * Map to hold registered modules. Use the class type of the containing
	 * module as the key.
	 */
	private Map<Class<?>, Object> _modules = new HashMap<Class<?>, Object>();

	//

	public void setRootContext(ApplicationContext applicationContext) {
		_rootContext = applicationContext;
	}

	public ApplicationContext getRootContext() {
		return _rootContext;
	}

	private ApplicationContext _rootContext;

	//

	private transient static Logger _logger = LoggerFactory
			.getLogger(ModuleRegistry.class);
}
